package net.sf.cpsolver.itc;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.text.DecimalFormat;
import java.util.Collection;
import java.util.Map;

import net.sf.cpsolver.ifs.solution.Solution;
import net.sf.cpsolver.ifs.solution.SolutionListener;
import net.sf.cpsolver.ifs.solver.Solver;
import net.sf.cpsolver.ifs.util.DataProperties;
import net.sf.cpsolver.ifs.util.JProf;
import net.sf.cpsolver.ifs.util.Progress;
import net.sf.cpsolver.ifs.util.ToolBox;

import org.apache.log4j.ConsoleAppender;
import org.apache.log4j.FileAppender;
import org.apache.log4j.Level;
import org.apache.log4j.Logger;
import org.apache.log4j.PatternLayout;

/**
 * Main class for all three competition tracks.
 * <br><br>
 * Usage:<br>
 * <ul>
 * java -Xmx256m -jar itc2007.jar problem input output [timeout] [seed]
 * </ul>
 * Parameters:
 * <ul>
 * <li>problem .. problem either ctt, tim, or exam (configuration file <problem>.properties is used)
 * <ul>
 *  <li>ctt for Curriculum based Course Timetabling (track 3)
 *  <li>tim for Post Enrollment based Course Timetabling (track 2)
 *  <li>exam for Examination Timetabling (track 1)
 *  </ul>
 *  <li>input .. input file
 *  <li>output .. output file (or folder when an output file is to be saved)
 *  <li>timeout .. time limit in seconds, optional (default is 276 seconds, that is for MacOS X 3GHz server)
 *  <li>seed .. random seed, optional (randomly generated by default)
 *  </ul>
 *  <br>
 *  Requirements:
 *  <ul>
 *  Java Runtime Environment (JRE) or Java SE Development Kit preferably from Sun,
 *  version 1.5 or later, can be downloaded and installed from http://java.sun.com/
 *  </ul>
 *  Examples:
 *  <ul>
 *  Curriculum based Course Timetabling, 445 seconds time limit, seed 12453312
 *  <ul>
 *   <li>
 *   java -Xmx256m -jar itc2007.jar ctt comp01.ctt comp01.out 445 12453312
 *   </li>
 *  </ul>
 *  Post Enrollment based Course Timetabling, 350 seconds time limit, no seed
 *  <ul>
 *   <li>
 *   java -Xmx256m -jar itc2007.jar tim comp-2007-2-1.tim comp-2007-2-1.sln 350
 *   </li>
 *  </ul>
 *  Examination Timetabling, 499 seconds time limit, seed 555
 *  <ul>
 *   <li>
 *   java -Xmx256m -jar itc2007.jar exam exam_comp_set1.exam exam_comp_set1.sln 499 555
 *   </li>
 *  </ul>
 * </ul>
 * 
 * @version
 * ITC2007 1.0<br>
 * Copyright (C) 2007 Tomas Muller<br>
 * <a href="mailto:muller@unitime.org">muller@unitime.org</a><br>
 * <a href="http://muller.unitime.org">http://muller.unitime.org</a><br>
 * <br>
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 3 of the License, or (at your option) any later version.
 * <br><br>
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 * <br><br>
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not see
 * <a href='http://www.gnu.org/licenses/'>http://www.gnu.org/licenses/</a>.
 */

@SuppressWarnings({ "rawtypes", "unchecked" })
public class ItcTest {
    private static String sProblem = null;
    private static File sInputFile = null;
    private static File sOutputFile = null;
    private static File sCSVFile = null;
    private static File sLogFile = null;
    private static long sSeed = generateSeed();
    private static long sTimeOut = -1;
    private static DataProperties sConfig = new DataProperties();
    private static Logger sLog = Logger.getLogger(ItcTest.class); 
    
    /** Generate random seed */
    public static long generateSeed() {
        //return System.currentTimeMillis();
        return Math.round(Long.MAX_VALUE * Math.random());
    }
    
    /** Setup Log4j logging */
    public static void setupLogging(File logFile, boolean info, boolean debug) {
        Logger root = Logger.getRootLogger();
        ConsoleAppender console = new ConsoleAppender(new PatternLayout("%m%n"));//%-5p %c{1}> %m%n
        console.setThreshold(info?Level.INFO:Level.WARN);
        root.addAppender(console);
        Logger.getLogger(JProf.class).setLevel(Level.ERROR);
        if (info || debug) {
            try {
                FileAppender file = new FileAppender(
                    new PatternLayout("%d{dd-MMM-yy HH:mm:ss.SSS} [%t] %-5p %c{2}> %m%n"), 
                    logFile.getPath(), 
                    false);
                file.setThreshold(Level.DEBUG);
                root.addAppender(file);
            } catch (IOException e) {
                sLog.fatal("Unable to configure logging, reason: "+e.getMessage(), e);
            }
        }
        if (!debug) root.setLevel(info?Level.INFO:Level.WARN);
    }
    
    /** Print usage */
    public static void printUsage() {
        System.out.println("Usage: java [options] -jar itc2007.jar problem input [output] [timeout] [seed]");
        System.out.println();
        System.out.println("  problem ... itc, tim, exam");
        System.out.println("  input ... input file");
        System.out.println("  output ... output file or folder");
        System.out.println();
        System.out.println("Additional options:");
        System.out.println("  -Dtimeout=X ... time to run in seconds");
        System.out.println("  -Dseed=X ... random generator seed");
        System.out.println("  -Dverbose=info ... enable info messages");
        System.out.println("  -Dverbose=debug ... enable debug messages");
        System.out.println();
        System.out.println("Examples:");
        System.out.println();
        System.out.println("Track1: (Examination Timetabling)");
        System.out.println("  java -jar itc2007.jar exam exam_comp_set1.exam exam_comp_set1.out");
        System.out.println();
        System.out.println("Track2: (Post Enrolment based Course Timetabling, time limit set to 288 seconds)");
        System.out.println("  java -Dtimeout=288 -jar itc2007.jar tim comp-2007-2-1.tim comp-2007-2-1.sol");
        System.out.println();
        System.out.println("Track3: (Curriculum based Course Timetabling, time limit set to 288 seconds, seed set to 123)");
        System.out.println("  java -Dseed=123 -Dtimeout=288 -jar itc2007.jar ctt comp01.ctt comp01.out");
    }
    
    /** Parse input arguments */
    public static boolean init(String args[]) {
        if (args==null || args.length<=1) {
            printUsage();
            return false;
        }
        sProblem = args[0];
        sInputFile = new File(args[1]);
        if (!sInputFile.exists()) {
            System.err.println("Input file '"+sInputFile+"' does not exist.");
            return false;
        }
        if (args.length>=5) {
            sSeed = Long.parseLong(args[4]);
        }
        if (args.length>=4) {
            sTimeOut = Long.parseLong(args[3]);
        }
        sSeed = Long.parseLong(System.getProperty("seed", String.valueOf(sSeed)));
        sTimeOut = Long.parseLong(System.getProperty("timeout", String.valueOf(sTimeOut)));
        if (ItcTest.class.getResource("/"+sProblem+".properties")!=null) {
            try {
                sConfig.load(ItcTest.class.getResourceAsStream("/"+sProblem+".properties"));
            } catch (IOException e) {
                System.err.println("Unable to read property file, reason: "+e.getMessage());
                e.printStackTrace(System.err);
            }
        }
        sConfig.putAll(System.getProperties());
        String ext = sConfig.getProperty("Model.Extension","out");
        
        if (args.length>=3) {
            sOutputFile = new File(args[2]);
            if (sOutputFile.exists() && sOutputFile.isDirectory()) {
                String name = sInputFile.getName();
                if (name.indexOf('.')>=0) name=name.substring(0, name.lastIndexOf('.'));
                sOutputFile = new File(sOutputFile, name+"_"+sSeed+"."+ext);
            }
        } else {
            String name = sInputFile.getName();
            if (name.indexOf('.')>=0) name=name.substring(0, name.lastIndexOf('.'));
            sOutputFile = new File(sInputFile.getParentFile(), name+"_"+sSeed+"."+ext);
        }
        if (sOutputFile.getParentFile()!=null) sOutputFile.getParentFile().mkdirs();

        sLogFile = new File(sOutputFile.getParentFile(), sOutputFile.getName().substring(0,sOutputFile.getName().lastIndexOf('.'))+".log");
        boolean debug = "debug".equals(System.getProperty("verbose"));
        boolean info = debug || "info".equals(System.getProperty("verbose"));
        setupLogging(sLogFile, info, debug);
        
        sCSVFile = new File(sOutputFile.getParentFile(),sInputFile.getName().substring(0, sInputFile.getName().lastIndexOf('.'))+".csv");
        
        ToolBox.setSeed(sSeed);
        sConfig.setProperty("General.Seed", String.valueOf(sSeed));
        sConfig.setProperty("General.Input", sInputFile.getPath());
        sConfig.setProperty("General.Output", sOutputFile.getPath());
        if (sTimeOut>=0)
            sConfig.setProperty("Termination.TimeOut", String.valueOf(sTimeOut));
        else
            sTimeOut = sConfig.getPropertyLong("Termination.TimeOut", -1);
        
        sLog.info("Problem: "+sProblem);
        sLog.info("Input:   "+sInputFile);
        sLog.info("Output:  "+sOutputFile);
        sLog.info("CSV:     "+sCSVFile);
        sLog.info("Log:     "+sLogFile);
        sLog.info("Seed:    "+sSeed);
        sLog.info("Timeout: "+sTimeOut);
        
        return true;
    }
    
    /** Create solver instance */
	private static Solver create() throws Exception {
        ItcModel model = (ItcModel<?,?>)Class.forName(sConfig.getProperty("Model.Class")).newInstance();
        model.setProperties(sConfig);
        if (!model.load(sInputFile)) {
            sLog.error("Unable to load input file.");
            return null;
        }
        
        Solver solver = new Solver(sConfig);
		Solution solution = new Solution(model);
        solver.setInitalSolution(solution);
        
        solver.currentSolution().addSolutionListener(new SolutionListener() {
            public void solutionUpdated(Solution solution) {}
            public void getInfo(Solution solution, Map info) {}
            public void getInfo(Solution solution, Map info, Collection variables) {}
            public void bestCleared(Solution solution) {}
            public void bestSaved(Solution solution) {
                ItcModel m = (ItcModel)solution.getModel();
                sLog.info("**BEST["+solution.getIteration()+"]** V:"+m.nrAssignedVariables()+"/"+m.variables().size()+", P:"+Math.round(m.getTotalValue())+" ("+m.csvLine()+")");
            }
            public void bestRestored(Solution solution) {}
        });
        
        return solver;
    }
    
    /** Solve problem */
	public static Solution solve() {
        try {
            Solver solver = create();
            
            solver.start();
            try {
                solver.getSolverThread().join();
            } catch (InterruptedException e) {}
            
            Solution solution = solver.currentSolution();
            solution.restoreBest();
            ((ItcModel)solution.getModel()).makeFeasible();
            solution.saveBest();
            
            return output(solver);
        } catch (Exception e) {
            sLog.error("Unable to solve problem, reason: "+e.getMessage(),e);
        }
        return null;
    }
    
    /** Output solution and some additional information */
	private static Solution output(Solver solver) throws Exception {
        Solution solution = solver.lastSolution();
        ItcModel model = (ItcModel)solution.getModel();
        Progress.removeInstance(model);
        if (solution.getBestInfo()==null) {
            sLog.error("No best solution found.");
            return null;
        }
        solution.restoreBest();
        
        sLog.info("Best solution:"+ToolBox.dict2string(solution.getExtendedInfo(),1));
        
        sLog.info("Best solution found after "+solution.getBestTime()+" seconds ("+solution.getBestIteration()+" iterations).");
        sLog.info("Number of assigned variables is "+solution.getModel().assignedVariables().size());
        sLog.info("Total value of the solution is "+solution.getModel().getTotalValue());
        
        if (sOutputFile!=null && !model.save(sOutputFile)) {
            sLog.error("Unable to save solution.");
        }
        
        if (sCSVFile!=null && model.cvsPrint()) {
            boolean ex = sCSVFile.exists();
            PrintWriter w = new PrintWriter(new FileWriter(sCSVFile,true));
            if (!ex)
                w.println("seed,timeout,time,iter,total,"+model.csvHeader());
            ItcModel m = (ItcModel)solution.getModel();
            DecimalFormat df = new DecimalFormat("0.00");
            w.println(
                    sSeed+","+
                    sTimeOut+","+
                    df.format(solution.getBestTime())+","+
                    solution.getBestIteration()+","+
                    Math.round(m.getTotalValue()+5000*m.unassignedVariables().size())+","+
                    model.csvLine());
            w.flush(); w.close();
        }
        
        return solution;
    }
    
    /** Stop solver and output solution when Ctrl^C is pressed. */
    private static class ShutdownHook extends Thread {
		Solver iSolver = null;
        public ShutdownHook(Solver solver) {
            setName("ShutdownHook");
            iSolver = solver;
        }
        public void run() {
            try {
                if (iSolver.isRunning()) iSolver.stopSolver();

                Solution solution = iSolver.currentSolution();
                solution.restoreBest();
                ((ItcModel)solution.getModel()).makeFeasible();
                solution.saveBest();
                
                output(iSolver);
            } catch (Exception e) {
                sLog.error("Unable to solve problem, reason: "+e.getMessage(),e);
            }
        }
    }
    
    /** Test given instance, return best found solution */
	public static Solution test(String instance, DataProperties properties, long seed, long timeout) {
        ToolBox.setSeed(seed);
        properties.setProperty("General.Seed", String.valueOf(seed));
        properties.setProperty("General.Input", instance);
        properties.remove("General.Output");
        properties.setProperty("Termination.TimeOut", String.valueOf(timeout));
        sSeed = seed;
        sTimeOut = timeout;
        sConfig = properties;
        sCSVFile = null;
        sInputFile = new File(instance);
        sOutputFile = null;
        sLogFile = null;
        Solution solution = solve();
        return solution;
    }
    
    /** Main method -- parse input arguments, create solver, solve, and output solution on exit */
	public static void main(String[] args) {
        try {
            if (init(args)) {
                Solver solver = create();
            
                Runtime.getRuntime().addShutdownHook(new ShutdownHook(solver));

                solver.start();
                
                solver.getSolverThread().join();
            }
        } catch (Exception e) {
            sLog.error("Unable to solve problem, reason: "+e.getMessage(),e);
        }
    }

}
